﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/Director.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using Haplous.Core;
using Haplous.Correctness;

using JetBrains.Annotations;

namespace Haplous
{
    /// <summary>
    ///     An object that manages the movements of a mobile element.
    /// </summary>
    [DebuggerDisplay("Director: Name={Name}")]
    public abstract class Director : ElementBase
    {
        /// <summary>
        ///     The empty size value.
        /// </summary>
        [NotNull]
        private static readonly SizeInt32 _emptySize = new SizeInt32(0, 0);

        /// <summary>
        ///     The empty position value.
        /// </summary>
        [NotNull]
        private static readonly PositionInt32 _emptyPosition = new PositionInt32(0, 0);

        /// <summary>
        ///     The Actor that this instance controls.
        /// </summary>
        [NotNull]
        private readonly Actor _actor;

        /// <summary>
        ///     The current direction in which the character is moving.
        /// </summary>
        [NotNull]
        private readonly Direction _currentDirection = new Direction();

        /// <summary>
        ///     The mode in which this controller will be active.
        /// </summary>
        [NotNull]
        private readonly Mode _mode;

        /// <summary>
        ///     The unique name that can be used to identify this object.  The name must not be <see langword="null" />, empty or
        ///     all whitespace.
        /// </summary>
        [NotNull]
        private readonly string _name;

        /// <summary>
        ///     This controller is the active controller for the Actor/
        /// </summary>
        private bool _isActive;

        /// <summary>
        ///     The direction in which the block was moving when the game started.
        /// </summary>
        private DirectionKind _startingDirection;

        /// <summary>
        ///     Initializes a new instance of the <see cref="ElementBase" /> class.
        /// </summary>
        /// <param name="game">
        ///     The game to which the class belongs.  The <paramref name="game" /> must not be
        ///     <see langword="null" />.
        /// </param>
        /// <param name="name">
        ///     The unique name that can be used to identify this object.  The <paramref name="name" /> must not be
        ///     <see langword="null" />, empty or all whitespace.
        /// </param>
        /// <param name="actor">The Actor to control.  Must not be <see langword="null" />.</param>
        protected Director([NotNull] Game game, [NotNull] string name, [NotNull] Actor actor) : base(game)
        {
            name.ValidateArgument("name", StringMustNotBe.NullOrEmptyOrWhiteSpace);
            actor.ValidateArgument("actor");

            game.ModeChanging += ModeChanging;
            game.ModeChanged += ModeChanged;
            game.Starting += GameStarting;
            game.Started += GameStarted;
            game.Stopping += GameStopping;
            game.Stopped += GameStopped;

            _mode = game.ActiveMode;
            _name = name;
            _actor = actor;
        }

        /// <summary>
        ///     The current direction in which the character is moving.
        /// </summary>
        [NotNull]
        public Direction CurrentDirection
        {
            get { return _currentDirection; }
        }

        /// <summary>
        ///     The mode in which this controller will be active.
        /// </summary>
        [NotNull]
        public Mode Mode
        {
            get { return _mode; }
        }

        /// <summary>
        ///     The Actor that this instance controls.
        /// </summary>
        [NotNull]
        public Actor Actor
        {
            get { return _actor; }
        }

        /// <summary>
        ///     This controller is the active controller for the Actor/
        /// </summary>
        public bool IsActive
        {
            get { return _isActive; }
            set { OnPropertyChanged("IsActive", ref _isActive, value); }
        }

        /// <summary>
        ///     The unique name that can be used to identify this object.  The name must not be <see langword="null" />, empty or
        ///     all whitespace.
        /// </summary>
        public override string Name
        {
            get { return _name; }
        }

        /// <summary>
        ///     Resets the direction to that in use at the start of the game or when <see cref="SnapDirection" /> was last called.
        /// </summary>
        public void ResetDirection()
        {
            _currentDirection.Value = _startingDirection;
        }

        /// <summary>
        ///     Snaps the direction for restoration later using the <see cref="ResetDirection" /> method.
        /// </summary>
        public void SnapDirection()
        {
            _startingDirection = _currentDirection.Value;
        }

        /// <summary>
        ///     Called when the game is about to start playing.
        /// </summary>
        protected override void OnGameStarting()
        {
            SnapDirection();
        }

        /// <summary>
        ///     Called when the game has stopped.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void GameStopped([CanBeNull] object sender, [NotNull] EventArgs e)
        {
            try
            {
                if(Game.ActiveMode == _mode)
                {
                    LogToDebug("Game stopped");

                    IsActive = false;
                    OnStopped();
                }
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Called when the game is stopping.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void GameStopping([CanBeNull] object sender, [NotNull] EventArgs e)
        {
            try
            {
                if(Game.ActiveMode == _mode)
                {
                    LogToDebug("Game stopping");

                    OnStopping();
                }
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Called when the game has started.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void GameStarted([CanBeNull] object sender, [NotNull] EventArgs e)
        {
            try
            {
                if(Game.ActiveMode == _mode)
                {
                    LogToDebug("Game started");

                    IsActive = true;
                    OnStarted();
                }
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Called when the game is starting.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void GameStarting([CanBeNull] object sender, [NotNull] EventArgs e)
        {
            try
            {
                if(Game.ActiveMode == _mode)
                {
                    LogToDebug("Game starting");

                    OnStarting();
                }
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Returns the solid, if any, found at the coordinates given.
        /// </summary>
        /// <param name="position">The position at which to test.</param>
        /// <param name="size">The size of the area to check.</param>
        /// <returns>A <see cref="SolidGraphicBlock" />An enumerable containing all solids founds in the area specified.</returns>
        [NotNull]
        public IEnumerable<SolidGraphicBlock> HitTest([NotNull] PositionInt32 position, [NotNull] SizeInt32 size)
        {
            position.ValidateArgument("position");
            size.ValidateArgument("size");

            foreach(var layer in Game.Layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var element in layer.Elements)
                {
                    var solid = element as SolidGraphicBlock;
                    if(solid != null)
                    {
                        if(position.IsOverlapping(size, solid.Position, solid.Size, position))
                            yield return solid;
                    }
                }
            }
        }

        /// <summary>
        ///     Called when the game changes mode.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="ModeChangeEventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void ModeChanged([CanBeNull] object sender, [NotNull] ModeChangeEventArgs e)
        {
            try
            {
                if(e.PreviousMode == _mode)
                    OnStopping();

                if(e.NewMode == _mode)
                    OnStarting();
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Called when the mode is about to change and this controller is to become active.
        /// </summary>
        /// <remarks>
        ///     The controller should not attempt to control the Actor, but can perform any internal actions necessary to prepare.
        /// </remarks>
        protected virtual void OnStarting()
        {}

        /// <summary>
        ///     Called when the mode is about to change and this controller is to stop being active.
        /// </summary>
        /// <remarks>
        ///     The controller is still in control of the Actor, but can perform any internal actions necessary to prepare to stop
        ///     controlling.
        /// </remarks>
        protected virtual void OnStopping()
        {}

        /// <summary>
        ///     Modes the game is changing mode.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="ModeChangeEventArgs" /> instance containing the event data.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void ModeChanging([CanBeNull] object sender, [NotNull] ModeChangeEventArgs e)
        {
            try
            {
                if(e.PreviousMode == _mode)
                {
                    IsActive = false;
                    OnStopped();
                }

                if(e.NewMode == _mode)
                {
                    IsActive = true;
                    OnStarted();
                }
            }
            catch(Exception ex)
            {
                ErrorManager.OnUnhandledException(ex);
            }
        }

        /// <summary>
        ///     Called when the mode has changed and this controller is now responsible for controlling the Actor.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        protected virtual void OnStarted()
        {}

        /// <summary>
        ///     Called when the mode has changed and this controller is no longer responsible for controlling the Actor.
        /// </summary>
        protected virtual void OnStopped()
        {}

        /// <summary>
        ///     Tries the move the element to a coordinate relative to the current position.
        /// </summary>
        /// <param name="relativePosition">The amount to move relative to the current position of the Actor.</param>
        /// <returns>A dictionary of collisions keyed on the item hit.</returns>
        [NotNull]
        protected IDictionary<ISolidGraphicBlock, OutcomeKind> Move([NotNull] PositionInt32 relativePosition)
        {
            return Move(relativePosition, _emptySize);
        }

        /// <summary>
        ///     Tries the move the element to a coordinate relative to the current position.
        /// </summary>
        /// <param name="relativeSize">The amount by which to change the size of the Actor</param>
        /// <returns>A dictionary of collisions keyed on the item hit.</returns>
        [NotNull]
        protected IDictionary<ISolidGraphicBlock, OutcomeKind> Move([NotNull] SizeInt32 relativeSize)
        {
            return Move(_emptyPosition, relativeSize);
        }

        /// <summary>
        ///     Tries the move the element to a coordinate relative to the current position.
        /// </summary>
        /// <param name="relativePosition">The amount to move relative to the current position of the Actor.</param>
        /// <param name="relativeSize">The amount by which to change the size of the Actor</param>
        /// <returns>A dictionary of collisions keyed on the item hit.</returns>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        [NotNull]
        protected IDictionary<ISolidGraphicBlock, OutcomeKind> Move([NotNull] PositionInt32 relativePosition, [NotNull] SizeInt32 relativeSize)
        {
            relativePosition.ValidateArgument("relativePosition");
            relativeSize.ValidateArgument("relativeSize");

            if(!_isActive)
                throw new LockedException("This controller is not active.");

            Lock();
            try
            {
                var newPosition = _actor.Position.Clone();
                newPosition.Set(newPosition.X + relativePosition.X, newPosition.Y + relativePosition.Y);

                var newSize = _actor.Size.Clone();
                newSize.Set(newSize.Width + relativeSize.Width, newSize.Height + relativeSize.Height);

                return _actor.Move(newPosition, newSize);
            }
            finally
            {
                Unlock();
            }
        }

        /// <summary>
        ///     Unlocks the sprites in the game for moving.
        /// </summary>
        private void Unlock()
        {
            foreach(var layer in Game.Layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var graphic in layer.Elements)
                {
                    var sprite = graphic as Actor;
                    if((sprite != null) && (sprite != _actor))
                    {
                        sprite.Position.FinishDirectorUpdate();
                        //Actor.Size.FinishDirectorUpdate();
                    }
                }
            }
        }

        /// <summary>
        ///     Locks the sprites in the game to prevent movement.
        /// </summary>
        private void Lock()
        {
            foreach(var layer in Game.Layers)
            {
                Debug.Assert(layer != null, "layer != null");
                foreach(var element in layer.Elements)
                {
                    var sprite = element as Actor;
                    if((sprite != null) && (sprite != _actor))
                    {
                        sprite.Position.StartDirectorUpdate();
                        //Actor.Size.StartDirectorUpdate();
                    }
                }
            }
        }

        /// <summary>
        ///     Ensures that a relative move to the current Actor position remains inside the game area.
        /// </summary>
        /// <param name="relativePosition">The relative position to which the Actor will move (relative to current position).</param>
        protected virtual void ConfineToGameArea([NotNull] PositionInt32 relativePosition)
        {
            relativePosition.ValidateArgument("relativePosition");

            if(Actor.Position.X + relativePosition.X < 0)
                relativePosition.X = Actor.Position.X;
            else
            {
                if(Actor.Position.X + (relativePosition.X + Actor.Size.Width) > Game.Size.Width)
                    relativePosition.X = Game.Size.Width - (Actor.Position.X + Actor.Size.Width);
            }

            if(Actor.Position.Y + relativePosition.Y < 0)
                relativePosition.Y = Actor.Position.Y;
            else
            {
                if(Actor.Position.Y + (relativePosition.Y + Actor.Size.Height) > Game.Size.Height)
                    relativePosition.Y = Game.Size.Height - (Actor.Position.Y + Actor.Size.Height);
            }
        }
    }
}